package no.nordicsemi.android.meshprovisioner;

import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import java.util.UUID;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Ignore;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import androidx.room.TypeConverters;
import no.nordicsemi.android.meshprovisioner.utils.MeshAddress;

import static androidx.room.ForeignKey.CASCADE;

/**
 * Defines a group in a mesh network
 */
@SuppressWarnings({"unused"})
@Entity(tableName = "groups",
        foreignKeys = @ForeignKey(entity = MeshNetwork.class,
                parentColumns = "mesh_uuid",
                childColumns = "mesh_uuid",
                onUpdate = CASCADE, onDelete = CASCADE),
        indices = {@Index("mesh_uuid")})
public class Group implements Parcelable {

    @PrimaryKey(autoGenerate = true)
    public int id = 0;

    @ColumnInfo(name = "name")
    @Expose
    @SerializedName("name")
    private String name = "Mesh Group";

    @ColumnInfo(name = "group_address")
    @Expose
    @SerializedName("address")
    private int address;

    @TypeConverters(MeshTypeConverters.class)
    @ColumnInfo(name = "group_address_label")
    @Expose(serialize = false, deserialize = false)
    private UUID addressLabel;

    @ColumnInfo(name = "parent_address")
    @Expose
    @SerializedName("parentAddress")
    private int parentAddress;

    @TypeConverters(MeshTypeConverters.class)
    @ColumnInfo(name = "parent_address_label")
    @Expose(serialize = false, deserialize = false)
    private UUID parentAddressLabel;

    @ColumnInfo(name = "mesh_uuid")
    @Expose
    @SerializedName("meshUuid")
    private String meshUuid;

    /**
     * Constructs a mesh group to be used for room db implementation
     *
     * @param id                 Autogenerated primary key id
     * @param address            Address of the group
     * @param addressLabel       Label UUID of the group address in the case of a virtual address
     * @param parentAddress      Parent address of the group
     * @param parentAddressLabel Label UUID of the parent address in the case of a virtual address
     * @param meshUuid           UUID of the mesh network
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public Group(final int id, final int address,
                 @Nullable final UUID addressLabel,
                 final int parentAddress,
                 @Nullable final UUID parentAddressLabel,
                 @NonNull final String meshUuid) throws IllegalArgumentException {
        this(address, addressLabel, parentAddress, parentAddressLabel, meshUuid);
        this.id = id;
    }

    /**
     * Constructs a mesh group
     *
     * @param address  Address of the group
     * @param meshUuid UUID of the mesh network
     */
    @Ignore
    public Group(final int address, @NonNull final String meshUuid) throws IllegalArgumentException {
        this(address, null, MeshAddress.UNASSIGNED_ADDRESS, null, meshUuid);
    }

    /**
     * Constructs a mesh group
     *
     * @param address       Address of the group
     * @param parentAddress Parent address of the group. If there is no parent group use {@link MeshAddress#UNASSIGNED_ADDRESS}
     * @param meshUuid      UUID of the mesh network
     */
    @Ignore
    public Group(final int address, final int parentAddress, @NonNull final String meshUuid) throws IllegalArgumentException {
        this(address, null, parentAddress, null, meshUuid);
    }

    /**
     * Constructs a mesh group
     *
     * @param address            Address of the group
     * @param parentAddressLabel Label UUID of the parent address in the case of a virtual address
     * @param meshUuid           UUID of the mesh network
     */
    @Ignore
    public Group(final int address, @NonNull final UUID parentAddressLabel, @NonNull final String meshUuid) throws IllegalArgumentException {
        this(address, null, MeshAddress.UNASSIGNED_ADDRESS, parentAddressLabel, meshUuid);
    }

    /**
     * Constructs a mesh group
     *
     * @param addressLabel  Label UUID of the address in the case of a virtual address
     * @param parentAddress Parent address of the group. If there is no parent group use {@link MeshAddress#UNASSIGNED_ADDRESS}
     * @param meshUuid      UUID of the mesh network
     */
    @Ignore
    public Group(@NonNull final UUID addressLabel, final int parentAddress, @NonNull final String meshUuid) throws IllegalArgumentException {
        this(MeshAddress.UNASSIGNED_ADDRESS, addressLabel, parentAddress, null, meshUuid);
    }

    /**
     * Constructs a mesh group
     *
     * @param addressLabel       Label UUID of the group address in the case of a virtual address
     * @param parentAddressLabel Label UUID of the parent address in the case of a virtual address
     * @param meshUuid           UUID of the mesh network
     */
    @Ignore
    public Group(@NonNull final UUID addressLabel, @Nullable final UUID parentAddressLabel, @NonNull final String meshUuid) throws IllegalArgumentException {
        this(MeshAddress.UNASSIGNED_ADDRESS, addressLabel, MeshAddress.UNASSIGNED_ADDRESS, parentAddressLabel, meshUuid);
    }

    /**
     * Constructs a mesh group
     *
     * @param address            Address of the group
     * @param addressLabel       Label UUID of the group address in the case of a virtual address
     * @param parentAddress      Parent address of the group. If there is no parent group use {@link MeshAddress#UNASSIGNED_ADDRESS}
     * @param parentAddressLabel Label UUID of the parent address in the case of a virtual address
     * @param meshUuid           UUID of the mesh network
     * @throws IllegalArgumentException for invalid arguments
     */
    private Group(final int address,
                  @Nullable final UUID addressLabel,
                  final int parentAddress,
                  @Nullable final UUID parentAddressLabel,
                  @NonNull final String meshUuid) throws IllegalArgumentException {
        if (!MeshAddress.isValidUnassignedAddress(address) && MeshAddress.isValidUnassignedAddress(parentAddress)) {
            if (address == parentAddress) {
                throw new IllegalArgumentException("The value of the parentAddress property shall not be equal to the value of the address property");
            }
        }

        if (addressLabel == null) {
            if (MeshAddress.isValidVirtualAddress(address)) {
                throw new IllegalArgumentException("Please provide a Label UUID when creating groups using virtual addresses");
            } else if (!MeshAddress.isValidGroupAddress(address)) {
                throw new IllegalArgumentException("Address of the group must be a valid group address or a virtual address");
            }
            this.address = address;
            generateParentAddressLabel(parentAddressLabel, parentAddress);
        } else {
            this.addressLabel = addressLabel;
            this.address = MeshAddress.generateVirtualAddress(addressLabel);
            generateParentAddressLabel(parentAddressLabel, parentAddress);
        }
        this.meshUuid = meshUuid;
    }

    private void generateParentAddressLabel(@Nullable final UUID parentAddressLabel, final int parentAddress) throws IllegalArgumentException {
        this.parentAddressLabel = parentAddressLabel;
        if (parentAddressLabel == null) {
            if (MeshAddress.isValidVirtualAddress(parentAddress)) {
                throw new IllegalArgumentException("Please provide a Label UUID for the parent address of this group");
            } else if (!MeshAddress.isValidGroupAddress(parentAddress) && !MeshAddress.isValidUnassignedAddress(parentAddress)) {
                throw new IllegalArgumentException("parentAddress value must be a group address, " +
                        "virtual address or an unassigned address");
            }
            this.parentAddress = parentAddress;
        } else {
            this.parentAddress = MeshAddress.generateVirtualAddress(parentAddressLabel);
        }
    }

    protected Group(Parcel in) {
        id = in.readInt();
        name = in.readString();
        address = in.readInt();
        addressLabel = readVirtualLabelFromParcelable(in, address);
        parentAddress = in.readInt();
        parentAddressLabel = readVirtualLabelFromParcelable(in, parentAddress);
        meshUuid = in.readString();
    }

    private UUID readVirtualLabelFromParcelable(Parcel in, final int address) {
        if (MeshAddress.isValidVirtualAddress(address)) {
            final ParcelUuid parcelUuid = in.readParcelable(ParcelUuid.class.getClassLoader());
            if (parcelUuid != null) {
                return parcelUuid.getUuid();
            }
        }
        return null;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
        dest.writeInt(address);
        writeVirtualLabelToParcel(dest, flags, address, addressLabel);
        dest.writeInt(parentAddress);
        writeVirtualLabelToParcel(dest, flags, parentAddress, parentAddressLabel);
        dest.writeString(meshUuid);
    }

    private void writeVirtualLabelToParcel(Parcel dest, int flags, final int address, final UUID addressLabel) {
        if (MeshAddress.isValidVirtualAddress(address)) {
            dest.writeParcelable(new ParcelUuid(addressLabel), flags);
        }
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<Group> CREATOR = new Creator<Group>() {
        @Override
        public Group createFromParcel(Parcel in) {
            return new Group(in);
        }

        @Override
        public Group[] newArray(int size) {
            return new Group[size];
        }
    };

    /**
     * Returns the provisionerUuid of the network
     *
     * @return provisionerUuid
     */
    public String getMeshUuid() {
        return meshUuid;
    }

    /**
     * Sets the provisionerUuid of the network
     *
     * @param meshUuid network provisionerUuid
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public void setMeshUuid(final String meshUuid) {
        this.meshUuid = meshUuid;
    }

    /**
     * Returns the address of the group
     *
     * @return 2 byte group address
     */
    public int getAddress() {
        return address;
    }

    /**
     * Returns the label UUID of the address of the group
     *
     * @return Label UUID of the group address if the group address is a virtual address or null otherwise
     */
    @Nullable
    public UUID getAddressLabel() {
        return addressLabel;
    }

    /**
     * Sets the group address label
     *
     * @param uuidLabel UUID label of the address
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public void setAddressLabel(@Nullable final UUID uuidLabel) {
        addressLabel = uuidLabel;
        if (uuidLabel != null) {
            address = MeshAddress.generateVirtualAddress(uuidLabel);
        }
    }

    public UUID getParentAddressLabel() {
        return parentAddressLabel;
    }

    /**
     * Sets the label UUID of the parent address
     *
     * @param parentAddressLabel Label UUID of the parent address
     */
    public void setParentAddressLabel(@Nullable final UUID parentAddressLabel) {
        this.parentAddressLabel = parentAddressLabel;
        if (parentAddressLabel != null) {
            parentAddress = MeshAddress.generateVirtualAddress(parentAddressLabel);
        }
    }

    /**
     * Returns address of the parent group if the group has one
     *
     * @return parent address
     */
    public int getParentAddress() {
        return parentAddress;
    }

    /**
     * Sets the parent address, if this group belongs to a parent group
     *
     * @param parentAddress address of the parent group
     */
    public void setParentAddress(final int parentAddress) {
        if (MeshAddress.isValidVirtualAddress(parentAddress)) {
            throw new IllegalArgumentException("Assigning a virtual address to parentAddress can be done only by setting a parent address label");
        } else if (!MeshAddress.isValidGroupAddress(parentAddress)) {
            throw new IllegalArgumentException("parentAddress must be a group address value");
        }
        this.parentAddress = parentAddress;
        this.parentAddressLabel = null;
    }

    /**
     * Returns the group name of a mesh network
     */
    public String getName() {
        return name;
    }

    /**
     * Sets a name to a mesh group
     */
    public void setName(@NonNull final String name) {
        this.name = name;
    }

}
